Hrvatski

Sveobuhvatan vodič za TypeScript generike. Saznajte o sintaksi, prednostima i naprednoj upotrebi za rad sa složenim tipovima podataka u globalnom razvoju.

TypeScript Generici: Ovladavanje složenim tipovima podataka za robusne aplikacije

TypeScript, nadskup JavaScripta, omogućuje programerima pisanje robusnijeg koda koji je lakši za održavanje pomoću statičkog tipiziranja. Među njegovim najmoćnijim značajkama su generici, koji vam omogućuju pisanje koda koji može raditi s različitim tipovima podataka, a da pritom zadrži sigurnost tipova. Ovaj vodič pruža sveobuhvatno istraživanje TypeScript generika, s fokusom na njihovu primjenu na složene tipove podataka u kontekstu globalnog razvoja softvera.

Što su generici?

Generici pružaju način pisanja ponovno iskoristivog koda koji može raditi s različitim tipovima. Umjesto pisanja zasebnih funkcija ili klasa za svaki tip koji želite podržati, možete napisati jednu funkciju ili klasu koja koristi tipske parametre. Ovi tipski parametri su zamjenski znakovi za stvarne tipove koji će se koristiti kada se funkcija ili klasa pozove ili instancira. Ovo je posebno korisno kada se radi sa složenim strukturama podataka gdje tip podataka unutar tih struktura može varirati.

Prednosti korištenja generika

Osnovna sintaksa generika

Osnovna sintaksa generika uključuje korištenje uglatih zagrada (< >) za deklariranje tipskih parametara. Ovi tipski parametri se obično nazivaju T, K, V, itd., ali možete koristiti bilo koji valjani identifikator. Evo jednostavnog primjera generičke funkcije:


function identity<T>(arg: T): T {
  return arg;
}

let myString: string = identity<string>("hello");
let myNumber: number = identity<number>(123);
let myBoolean: boolean = identity<boolean>(true);

console.log(myString); // Izlaz: hello
console.log(myNumber); // Izlaz: 123
console.log(myBoolean); // Izlaz: true

U ovom primjeru, <T> deklarira tipski parametar nazvan T. Funkcija identity prima argument tipa T i vraća vrijednost tipa T. Prilikom pozivanja funkcije, možete eksplicitno navesti tipski parametar (npr. identity<string>) ili dopustiti TypeScriptu da ga zaključi na temelju tipa argumenta.

Rad sa složenim tipovima podataka

Generici postaju posebno vrijedni kada se radi sa složenim tipovima podataka kao što su polja, objekti i sučelja. Istražimo neke uobičajene scenarije:

Generička polja

Možete koristiti generike za stvaranje funkcija ili klasa koje rade s poljima različitih tipova:


function arrayToString<T>(arr: T[]): string {
  return arr.join(", ");
}

let numberArray: number[] = [1, 2, 3, 4, 5];
let stringArray: string[] = ["apple", "banana", "cherry"];

console.log(arrayToString(numberArray)); // Izlaz: 1, 2, 3, 4, 5
console.log(arrayToString(stringArray)); // Izlaz: apple, banana, cherry

Ovdje, funkcija arrayToString prima polje tipa T[] i vraća string reprezentaciju polja. Ova funkcija radi s poljima bilo kojeg tipa, što je čini vrlo ponovno iskoristivom.

Generički objekti

Generici se također mogu koristiti za definiranje funkcija ili klasa koje rade s objektima različitih oblika:


interface Person {
  name: string;
  age: number;
  country: string; // Dodana zemlja za globalni kontekst
}

interface Product {
  id: number;
  name: string;
  price: number;
  currency: string; // Dodana valuta za globalni kontekst
}

function displayInfo<T extends { name: string }>(item: T): void {
  console.log(`Name: ${item.name}`);
}

let person: Person = { name: "Alice", age: 30, country: "USA" };
let product: Product = { id: 1, name: "Laptop", price: 1200, currency: "USD" };

displayInfo(person); // Izlaz: Name: Alice
displayInfo(product); // Izlaz: Name: Laptop

U ovom primjeru, funkcija displayInfo prima objekt tipa T koji mora imati svojstvo name tipa string. Klauzula extends { name: string } je ograničenje, koje specificira minimalne zahtjeve za tipski parametar T. To osigurava da funkcija može sigurno pristupiti svojstvu name.

Napredna upotreba generika

TypeScript generici nude naprednije značajke koje vam omogućuju stvaranje još fleksibilnijeg i moćnijeg koda. Istražimo neke od tih značajki:

Višestruki tipski parametri

Možete definirati funkcije ili klase s višestrukim tipskim parametrima:


function merge<T, U>(obj1: T, obj2: U): T & U {
  return { ...obj1, ...obj2 };
}

interface Name {
  firstName: string;
}

interface Age {
  age: number;
}

const person: Name = { firstName: "Bob" };
const details: Age = { age: 42 };

const merged = merge(person, details);
console.log(merged.firstName); // Izlaz: Bob
console.log(merged.age); // Izlaz: 42

Funkcija merge prima dva objekta tipova T i U i vraća novi objekt koji sadrži svojstva oba objekta. Ovo je moćan način za kombiniranje podataka iz različitih izvora.

Generička ograničenja

Kao što je ranije prikazano, ograničenja vam omogućuju da ograničite tipove koji se mogu koristiti s generičkim tipskim parametrom. To osigurava da generički kod može sigurno raditi na navedenim tipovima.


interface Lengthwise {
  length: number;
}

function loggingIdentity<T extends Lengthwise>(arg: T): T {
  console.log(arg.length);
  return arg;
}

loggingIdentity([1, 2, 3]); // Izlaz: 3
loggingIdentity("hello"); // Izlaz: 5
// loggingIdentity(123); // Greška: Argument tipa 'number' nije dodjeljiv parametru tipa 'Lengthwise'.

Funkcija loggingIdentity prima argument tipa T koji mora imati svojstvo length tipa number. To osigurava da funkcija može sigurno pristupiti svojstvu length.

Generičke klase

Generici se također mogu koristiti s klasama:


class DataStorage<T> {
  private data: T[] = [];

  addItem(item: T) {
    this.data.push(item);
  }

  removeItem(item: T) {
    this.data = this.data.filter(d => d !== item);
  }

  getItems(): T[] {
    return [...this.data];
  }
}

const textStorage = new DataStorage<string>();
textStorage.addItem("apple");
textStorage.addItem("banana");
textStorage.removeItem("apple");
console.log(textStorage.getItems()); // Izlaz: [ 'banana' ]

const numberStorage = new DataStorage<number>();
numberStorage.addItem(1);
numberStorage.addItem(2);
numberStorage.removeItem(1);
console.log(numberStorage.getItems()); // Izlaz: [ 2 ]

Klasa DataStorage može pohranjivati podatke bilo kojeg tipa T. To vam omogućuje stvaranje ponovno iskoristivih struktura podataka koje su tipski sigurne.

Generička sučelja

Generička sučelja su korisna za definiranje ugovora koji mogu raditi s različitim tipovima. Na primjer:


interface Result<T, E> {
  success: boolean;
  data?: T;
  error?: E;
}

interface User {
  id: number;
  username: string;
  email: string;
}

interface ErrorMessage {
  code: number;
  message: string;
}

function fetchUser(id: number): Result<User, ErrorMessage> {
  if (id === 1) {
    return { success: true, data: { id: 1, username: "john.doe", email: "john.doe@example.com" } };
  } else {
    return { success: false, error: { code: 404, message: "User not found" } };
  }
}

const userResult = fetchUser(1);
if (userResult.success) {
  console.log(userResult.data.username);
} else {
  console.log(userResult.error.message);
}

Sučelje Result definira generičku strukturu za predstavljanje ishoda operacije. Može sadržavati ili podatke tipa T ili grešku tipa E. Ovo je uobičajen obrazac za rukovanje asinkronim operacijama ili operacijama koje mogu ne uspjeti.

Pomoćni tipovi i generici

TypeScript pruža nekoliko ugrađenih pomoćnih tipova koji dobro rade s genericima. Ovi pomoćni tipovi mogu vam pomoći u transformaciji i manipulaciji tipovima na moćne načine.

Partial<T>

Partial<T> čini sva svojstva tipa T opcionalnima:


interface Person {
  name: string;
  age: number;
}

type PartialPerson = Partial<Person>;

const partialPerson: PartialPerson = { name: "Alice" }; // Valjano

Readonly<T>

Readonly<T> čini sva svojstva tipa T samo za čitanje (readonly):


interface Person {
  name: string;
  age: number;
}

type ReadonlyPerson = Readonly<Person>;

const readonlyPerson: ReadonlyPerson = { name: "Bob", age: 42 };
// readonlyPerson.age = 43; // Greška: Nije moguće dodijeliti vrijednost svojstvu 'age' jer je to svojstvo samo za čitanje.

Pick<T, K>

Pick<T, K> odabire skup svojstava K iz tipa T:


interface Person {
  name: string;
  age: number;
  email: string;
}

type NameAndAge = Pick<Person, "name" | "age">;

const nameAndAge: NameAndAge = { name: "Charlie", age: 28 };

Omit<T, K>

Omit<T, K> uklanja skup svojstava K iz tipa T:


interface Person {
  name: string;
  age: number;
  email: string;
}

type PersonWithoutEmail = Omit<Person, "email">;

const personWithoutEmail: PersonWithoutEmail = { name: "David", age: 35 };

Record<K, T>

Record<K, T> stvara tip s ključevima K i vrijednostima tipa T:


type CountryCodes = "US" | "CA" | "UK" | "DE" | "FR" | "JP" | "CN" | "IN" | "BR" | "AU"; // Prošireni popis za globalni kontekst
type Currency = "USD" | "CAD" | "GBP" | "EUR" | "JPY" | "CNY" | "INR" | "BRL" | "AUD"; // Prošireni popis za globalni kontekst

type CurrencyMap = Record<CountryCodes, Currency>;

const currencyMap: CurrencyMap = {
  "US": "USD",
  "CA": "CAD",
  "UK": "GBP",
  "DE": "EUR",
  "FR": "EUR",
  "JP": "JPY",
  "CN": "CNY",
  "IN": "INR",
  "BR": "BRL",
  "AU": "AUD",
};

Mapirani tipovi

Mapirani tipovi omogućuju vam transformaciju postojećih tipova iteriranjem preko njihovih svojstava. Ovo je moćan način za stvaranje novih tipova temeljenih na postojećima. Na primjer, možete stvoriti tip koji sva svojstva drugog tipa čini samo za čitanje (readonly):


interface Person {
  name: string;
  age: number;
}

type ReadonlyPerson = {
  readonly [K in keyof Person]: Person[K];
};

const readonlyPerson: ReadonlyPerson = { name: "Eve", age: 25 };
// readonlyPerson.age = 26; // Greška: Nije moguće dodijeliti vrijednost svojstvu 'age' jer je to svojstvo samo za čitanje.

U ovom primjeru, [K in keyof Person] iterira preko svih ključeva sučelja Person, a Person[K] pristupa tipu svakog svojstva. Ključna riječ readonly čini svako svojstvo samo za čitanje.

Uvjetni tipovi

Uvjetni tipovi omogućuju vam definiranje tipova na temelju uvjeta. Ovo je moćan način za stvaranje tipova koji se prilagođavaju različitim scenarijima.


type NonNullable<T> = T extends null | undefined ? never : T;

type MaybeString = string | null | undefined;
type StringType = NonNullable<MaybeString>; // string

function getValue<T>(value: T): NonNullable<T> {
  if (value == null) { // Obrađuje i null i undefined
    throw new Error("Value cannot be null or undefined");
  }
  return value as NonNullable<T>;
}

try {
  const validValue = getValue("hello");
  console.log(validValue.toUpperCase()); // Izlaz: HELLO

  const invalidValue = getValue(null); // Ovo će baciti grešku
  console.log(invalidValue); // Ova linija se neće izvršiti
} catch (error: any) {
  console.error(error.message); // Izlaz: Value cannot be null or undefined
}

U ovom primjeru, tip NonNullable<T> provjerava je li T jednak null ili undefined. Ako jest, vraća never, što znači da tip nije dopušten. Inače, vraća T. To vam omogućuje stvaranje tipova za koje je zajamčeno da ne mogu biti null.

Najbolje prakse za korištenje generika

Evo nekoliko najboljih praksi koje treba imati na umu prilikom korištenja generika:

Primjeri u globalnom kontekstu

Pogledajmo neke primjere kako se generici mogu koristiti u globalnom kontekstu:

Konverzija valuta


interface ConversionRate {
  rate: number;
  fromCurrency: string;
  toCurrency: string;
}

function convertCurrency<T extends ConversionRate>(amount: number, rate: T): number {
  return amount * rate.rate;
}

const usdToEurRate: ConversionRate = { rate: 0.85, fromCurrency: "USD", toCurrency: "EUR" };
const amountInUSD = 100;
const amountInEUR = convertCurrency(amountInUSD, usdToEurRate);
console.log(`${amountInUSD} USD is equal to ${amountInEUR} EUR`); // Izlaz: 100 USD is equal to 85 EUR

Formatiranje datuma


interface DateFormatOptions {
  locale: string;
  options: Intl.DateTimeFormatOptions;
}

function formatDate<T extends DateFormatOptions>(date: Date, format: T): string {
  return date.toLocaleDateString(format.locale, format.options);
}

const currentDate = new Date();

const usDateFormat: DateFormatOptions = { locale: "en-US", options: { year: 'numeric', month: 'long', day: 'numeric' } };
const germanDateFormat: DateFormatOptions = { locale: "de-DE", options: { year: 'numeric', month: 'long', day: 'numeric' } };
const japaneseDateFormat: DateFormatOptions = { locale: "ja-JP", options: { year: 'numeric', month: 'long', day: 'numeric' } };

console.log("US Date: " + formatDate(currentDate, usDateFormat));
console.log("German Date: " + formatDate(currentDate, germanDateFormat));
console.log("Japanese Date: " + formatDate(currentDate, japaneseDateFormat));

Usluga prevođenja


interface Translation {
  [key: string]: string; // Omogućuje dinamičke jezične ključeve
}

interface LanguageData<T extends Translation> {
  languageCode: string;
  translations: T;
}

const englishTranslations: Translation = {
  "hello": "Hello",
  "goodbye": "Goodbye",
  "welcome": "Welcome to our website!"
};

const spanishTranslations: Translation = {
  "hello": "Hola",
  "goodbye": "Adiós",
  "welcome": "¡Bienvenido a nuestro sitio web!"
};

const frenchTranslations: Translation = {
  "hello": "Bonjour",
  "goodbye": "Au revoir",
  "welcome": "Bienvenue sur notre site web !"
};


const languageData: LanguageData<typeof englishTranslations>[] = [
  {languageCode: "en", translations: englishTranslations },
  {languageCode: "es", translations: spanishTranslations },
  {languageCode: "fr", translations: frenchTranslations}
];

function translate<T extends Translation>(key: string, languageCode: string, languageData: LanguageData<T>[]): string {
  const lang = languageData.find(lang => lang.languageCode === languageCode);
  if (!lang) {
    return `Translation for ${key} in ${languageCode} not found.`;
  }
  return lang.translations[key] || `Translation for ${key} not found.`;
}

console.log(translate("hello", "en", languageData)); // Izlaz: Hello
console.log(translate("hello", "es", languageData)); // Izlaz: Hola
console.log(translate("welcome", "fr", languageData)); // Izlaz: Bienvenue sur notre site web !
console.log(translate("missingKey", "de", languageData)); // Izlaz: Translation for missingKey in de not found.

Zaključak

TypeScript generici su moćan alat za pisanje ponovno iskoristivog, tipski sigurnog koda koji može raditi sa složenim tipovima podataka. Razumijevanjem osnovne sintakse, naprednih značajki i najboljih praksi generika, možete značajno poboljšati kvalitetu i održivost svojih TypeScript aplikacija. Pri razvoju aplikacija za globalnu publiku, generici vam mogu pomoći u rukovanju različitim formatima podataka i kulturnim konvencijama, osiguravajući besprijekorno korisničko iskustvo za sve.